psql利用時にサーバ証明書の検証を強制してみた

psql利用時にサーバ証明書の検証を強制してみた

Clock Icon2024.08.31

こんにちは。中村です。

はじめに

RDSでpostgreSQLを利用する場合、エンジンバージョンが15.x以上になると、SSL/TLS接続がデフォルト設定で強制となります。
普段何気なくpsqlを利用してRDSへ接続しているけれど、証明書の検証ってどうなっているの?と疑問に思うことがあり、検証してみました。

結論

  • psqlではデフォルトで証明書の検証を行なっていませんでした
  • psqlでサーバ証明書の検証を行うためには、「PGSSLMODE=verify-full」など明示的に指定する必要があります

調べてみた

psqlはデフォルトでサーバ証明書を検証していない

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/PostgreSQL.Concepts.General.SSL.html#PostgreSQL.Concepts.General.SSL.Connecting

使用されるデフォルトの sslmode モードは、libpq ベースのクライアント (psql など) と JDBC では異なります。libpq ベースのクライアントはデフォルトで prefer に設定されますが、JDBC クライアントはデフォルトで verify-full に設定されます。

AWS公式ドキュメントにおいて、sslmodeが、利用するクライアントソフトによって異なる旨、説明されていました。

https://www.postgresql.jp/document/16/html/libpq-ssl.html

デフォルトではPostgreSQLはサーバ証明書の検証をまったく行いません。 これは、(例えば、DNSレコードを変更したり、もしくはサーバのIPアドレスを乗っ取ったりして)クライアントに知られずにサーバの身元をなりすませることを意味します。

postgreSQLのドキュメントにおいても、デフォルトでサーバ証明書の検証が実施されていない旨、案内がされています。

普段何気なくpsqlを利用していて、「SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)」などSSL通信できているようなレスポンスがあっても、実はデフォルト設定だと、サーバ証明書の検証をしていなかったのですね。

話の脱線

そもそもなぜ、
私がpsqlで証明書の検証がされているかどうか自体に気になり始めたのかというと、
RDSのルート証明書ってどこに保存されているのだろうか?
ということに疑問に感じたからでした。
そのとき、「/etc/pki/」配下で管理しているのだろうなと思ったものの、ただ、証明書の中身を確認するのは大変かなと思いました。
そこで、ディレクトリ自体をリネーム(もしくは、明らかに異なるルート証明書1つだけを設定)してエラーとなることを確認してみよう!という方針でひとまずpkiディレクトリ名を変更してみました。
結果、相変わらず、SSL/TLS通信ができてしまった。
ここで、前提が間違っているかもしれないと、psqlのSSL通信の仕組みを振り返ることにしたという流れでした。
結論は記載の通りです。

ちなみに、AWS CloudShellで検証していました。
AWS公式ドキュメントで明示的な案内は、見つけられませんでしたが、
「/certs/rds-global.pem」にもRDS用のCA証明書が保存されていそうです。
AWS公式からは、証明書バンドルをダウンロードする方法が案内されています。

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html

データベースに接続するときに使用する証明書バンドルをダウンロードします。証明書バンドルをダウンロードするには、 AWS リージョン による証明書バンドル. を参照してください。

やってみる

ということで、psqlを利用する際は、デフォルトにてサーバ証明書の検証がされていないことが判明したので、明示的にsslmodeの設定した通信を試してみます。

前提条件

  • psql (PostgreSQL) 15.8
  • PostgreSQL 15.8

やってみた

準備

本記事の実行環境として、RDSをパブリックサブネットに配置し、AWS CloudShellからpsql接続を試します。
RDSをプライベートサブネットに配置したい場合は、RDSを配置したVPC内部にEC2などのpsqlを実行できる環境を別途ご用意ください。
※通常、DBサーバはプライベートサブネットに配置することを推奨します。

まず、CloudShellのグローバルIPアドレスを下記コマンドを実行して確認しておきます。

curl http://checkip.amazonaws.com/

次に、RDSサーバを構築します。
下記CloudFormationテンプレートを利用する場合は、MyIPにCloudShellのグローバルIPアドレス(xxx.xxx.xxx.xxx/32)を記載してください。
合わせて、RDSMasterUserPasswordも入力ください。

CloudFormationテンプレート参考
AWSTemplateFormatVersion: "2010-09-09"

Description: >-
  AWS CloudFormation Template: 
  This template make temp RDS.

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      -
        Label:
          default: Common Configuration
        Parameters:
          - AppName
          - MyIP
          - RDSMasterUserPassword

Parameters:
  AppName: 
    Description: Name of this application.
    Type: String
    Default: rds-playground
  MyIP:
    Description: IP address allowed to connect
    Type: String
  RDSMasterUserPassword:
    Type: String
    NoEcho: true
    Description: The database admin account password

Mappings:
  SubnetConfig:
    VPC:
      CIDR: 10.20.0.0/16
    PublicSubnet1:
      CIDR: 10.20.11.0/24
    PublicSubnet2:
      CIDR: 10.20.12.0/24

Resources:
# ==========
# VPC
# ==========
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      EnableDnsSupport: true
      EnableDnsHostnames: true
      CidrBlock: !FindInMap [SubnetConfig, VPC, CIDR]
      Tags:
        - Key: Name
          Value: !Sub ${AppName}-VPC
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub ${AppName}-VPC-IGW
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId:
         !Ref VPC
      InternetGatewayId:
         !Ref InternetGateway
# ==========
# Public Subnet
# ==========
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select [0, !GetAZs ""]
      VpcId: !Ref VPC
      CidrBlock: !FindInMap [SubnetConfig, PublicSubnet1, CIDR]
      Tags:
        - Key: Name
          Value: !Sub ${AppName}-VPC-PVTSB1
        - Key: Network
          Value: Public
  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select [1, !GetAZs ""]
      VpcId: !Ref VPC
      CidrBlock: !FindInMap [SubnetConfig, PublicSubnet2, CIDR]
      Tags:
        - Key: Name
          Value: !Sub ${AppName}-VPC-PVTSB2
        - Key: Network
          Value: Public
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags: 
        - Key: Name
          Value: !Sub ${AppName}-public-rtb
      VpcId: !Ref VPC
  Route:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
      RouteTableId: !Ref PublicRouteTable
  PublicSubnetRouteTableAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicRouteTable
  PublicSubnetRouteTableAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet2
      RouteTableId: !Ref PublicRouteTable
# ==========
# RDS
# ==========
  RDSSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: "Security group"
      GroupName: !Sub ${AppName}-SG
      SecurityGroupIngress:
        - FromPort: 5432
          IpProtocol: tcp
          CidrIp: !Ref MyIP
          ToPort: 5432
      VpcId: !Ref VPC
  DBInstance:
    Type: AWS::RDS::DBInstance
    Properties:
      AllocatedStorage: 20
      DBInstanceClass: db.t4g.micro
      DBInstanceIdentifier: !Sub ${AppName}-rds-instance
      DBName: postgres
      DBParameterGroupName: !Ref DBParameterGroup
      DBSubnetGroupName: !Ref DBSubnetGroup
      Engine: postgres
      EngineVersion: 15.8
      MasterUsername: postgres
      MasterUserPassword: !Ref RDSMasterUserPassword
      MultiAZ: false
      PubliclyAccessible: true
      StorageType: gp3
      VPCSecurityGroups: 
        - !Ref RDSSecurityGroup
    DeletionPolicy: Delete
# ==========
# DB Subnet Group
# ==========
  DBSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription: "Subnet group"
      DBSubnetGroupName: !Sub ${AppName}-sbntgroup
      SubnetIds:
        - !Ref PublicSubnet1
        - !Ref PublicSubnet2
# ==========
# DB Parameter Group
# ==========
  DBParameterGroup:
    Type: AWS::RDS::DBParameterGroup
    Properties:
      DBParameterGroupName: !Sub ${AppName}-prmgrp
      Description: "Parameter group"
      Family: postgres15
      Parameters:
        timezone: Asia/Tokyo

やってみた

①デフォルト設定で接続してみる。

[cloudshell-user@ip-10-130-53-224 ~]$ psql -h rds-playground-rds-instance.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com -p 5432 -U postgres -d postgres
Password for user postgres: 
psql (15.8)
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, compression: off)
Type "help" for help.

postgres=>

②SSLモードをverify-fullにしてみる

[cloudshell-user@ip-10-130-53-224 ~]$ PGSSLMODE=verify-full psql -h rds-playground-rds-instance.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com -p 5432 -U postgres -d postgres
psql: error: connection to server at "rds-playground-rds-instance.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com" (xxx.xxx.xxx.xxx), port 5432 failed: root certificate file "/home/cloudshell-user/.postgresql/root.crt" does not exist
Either provide the file or change sslmode to disable server certificate verification.
[cloudshell-user@ip-10-130-53-224 ~]$

psqlのデフォルトファイルパスに証明書がないためエラーとなりました。

③SSLモードをverify-fullで証明書を指定して接続する

今回構築したRDSの認証機関は「rds-ca-rsa2048-g1」でした。
下記URLを参考に、「global-bundle.pem」をダウンロードします。

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html

続いて、ダウンロードした証明書をCloudShellにアップロードします。

[cloudshell-user@ip-10-130-53-224 ~]$ PGSSLMODE=verify-full PGSSLROOTCERT=/home/cloudshell-user/global-bundle.pem psql -h rds-playground-rds-instance.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com -p 5432 -U postgres -d postgres
Password for user postgres: 
psql (15.8)
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, compression: off)
Type "help" for help.

postgres=>

④SSLモードをverify-fullで証明書(別リージョンの証明書)を指定して接続する

③と同様の手順にて、証明書のみ米国東部 (バージニア北部)の証明書を利用してみます。

[cloudshell-user@ip-10-130-53-224 ~]$ PGSSLMODE=verify-full PGSSLROOTCERT=/home/cloudshell-user/us-east-1-bundle.pem psql -h rds-playground-rds-instance.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com -p 5432 -U postgres -d postgres
psql: error: connection to server at "rds-playground-rds-instance.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com" (xxx.xxx.xxx.xxx), port 5432 failed: SSL error: certificate verify failed
[cloudshell-user@ip-10-130-53-224 ~]$

証明書のパスが合わないのでエラーとなりました。

さいごに

今回の実験を通して、psqlを利用する場合は、sslmodeを明示的に指定することでサーバ証明書の検証ができることがわかりました。
ただし、15.x以上ではデフォルト設定にてTLS通信が強制されるけれども、クライアントの設定によってはサーバ証明書を検証しません。
利便性とのトレードオフにはなりますが、運用ポリシーに応じてsslmodeを設定すると良いでしょう。
今回の検証が、どなたかの参考になると嬉しいです。

参考

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.